package de.pellepelster.hudson.trafficlights;

import java.io.StringReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

/**
 * This Java based daemon aggregates the status of one ore more jobs from a hudson continous
 * integration server into a single status (red, yellow, green) and invokes a shell script on status
 * change.
 * 
 * @author Christian Pelster
 * @version $Rev$, $Date$
 */
public class HudsonTrafficLightsWorker implements Runnable {

	/**
	 * Maps hudson color codes to traffic light colors
	 * 
	 * @author pelle
	 */
	public enum JOB_COLOR {

		ABORTED(TRAFFIC_LIGHT_COLORS.YELLOW),
		ABORTED_ANIME(TRAFFIC_LIGHT_COLORS.YELLOW),

		BLUE(TRAFFIC_LIGHT_COLORS.GREEN),
		BLUE_ANIME(TRAFFIC_LIGHT_COLORS.GREEN, true),

		DISABLED(TRAFFIC_LIGHT_COLORS.GREEN),
		DISABLED_ANIME(TRAFFIC_LIGHT_COLORS.GREEN, true),

		GREY(TRAFFIC_LIGHT_COLORS.GREEN),
		GREY_ANIME(TRAFFIC_LIGHT_COLORS.GREEN, true),

		RED(TRAFFIC_LIGHT_COLORS.RED),
		RED_ANIME(TRAFFIC_LIGHT_COLORS.RED, true),

		YELLOW(TRAFFIC_LIGHT_COLORS.YELLOW),
		YELLOW_ANIME(TRAFFIC_LIGHT_COLORS.YELLOW);

		private final TRAFFIC_LIGHT_COLORS trafficLightColor;
		private final boolean building;

		private JOB_COLOR(TRAFFIC_LIGHT_COLORS trafficLightColor) {
			this(trafficLightColor, false);
		}

		private JOB_COLOR(TRAFFIC_LIGHT_COLORS trafficLightColor, boolean building) {
			this.trafficLightColor = trafficLightColor;
			this.building = building;
		}

		public TRAFFIC_LIGHT_COLORS getTrafficLightColor() {
			return trafficLightColor;
		};

		public boolean isBuilding() {
			return building;
		}

	}

	public enum TRAFFIC_LIGHT_COLORS {

		RED(4),
		YELLOW(3),
		GREEN(2),
		ALL(1);

		private final int severity;

		private TRAFFIC_LIGHT_COLORS(int severity) {
			this.severity = severity;
		}

		public int getSeverity() {
			return severity;
		}

	}

	private DocumentBuilder builder;
	private final HttpClient httpclient;
	private final HttpGet httpget;
	private final List<String> jobs;
	private final int pollingInterval;
	private boolean stopWorker = false;
	private final String wrapperScript;
	private final int wrapperScriptTimeout;
	private XPathExpression xpathExpression;
	private final static String JOB_COLOR_NODE_NAME = "color";
	private final static String JOB_NAME_NODE_NAME = "name";
	private static Logger log = Logger.getLogger(HudsonTrafficLights.class);

	public HudsonTrafficLightsWorker(String hudsonUrl, int pollingInterval, List<String> jobs,
			String wrapperScript, int wrapperScriptTimeout) {
		super();
		this.wrapperScript = wrapperScript;
		this.pollingInterval = pollingInterval;
		this.jobs = jobs;
		this.wrapperScriptTimeout = wrapperScriptTimeout;

		httpclient = new DefaultHttpClient();
		httpget = new HttpGet(String.format("%s/api/xml", hudsonUrl));

		DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
		domFactory.setNamespaceAware(true);
		try {
			builder = domFactory.newDocumentBuilder();
		}
		catch (ParserConfigurationException e) {
			log.fatal("failed to create document builder", e);
			System.exit(1);
		}

		XPath xpath = XPathFactory.newInstance().newXPath();
		try {
			xpathExpression = xpath.compile("//job");
		}
		catch (XPathExpressionException e) {
			log.fatal("failed to create xpath expression", e);
			System.exit(1);
		}

		initTrafficLights();
	}

	private void createCommandLineArguments(CommandLine commandLine, boolean green, boolean yellow,
			boolean red) {

		if (green) {
			commandLine.addArgument("1");
		}
		else {
			commandLine.addArgument("0");
		}

		if (yellow) {
			commandLine.addArgument("1");
		}
		else {
			commandLine.addArgument("0");
		}

		if (red) {
			commandLine.addArgument("1");
		}
		else {
			commandLine.addArgument("0");
		}

	}

	private Map<String, JOB_COLOR> getJobStatusMap(NodeList jobNodes) {

		Map<String, JOB_COLOR> result = new HashMap<String, JOB_COLOR>();

		for (int i = 0; i < jobNodes.getLength(); i++) {

			Node jobNode = jobNodes.item(i);
			String jobName = null;
			JOB_COLOR jobColor = null;

			for (int o = 0; o < jobNode.getChildNodes().getLength(); o++) {

				String nodeName = jobNode.getChildNodes().item(o).getNodeName();
				String nodeValue = jobNode.getChildNodes().item(o).getTextContent();

				if (JOB_NAME_NODE_NAME.equals(nodeName)) {
					jobName = nodeValue;
				}
				if (JOB_COLOR_NODE_NAME.equals(nodeName)) {
					try {
						jobColor = JOB_COLOR.valueOf(nodeValue.toUpperCase());
					}
					catch (IllegalArgumentException e) {
						log.error(String.format("unknown job color '%s'", nodeValue));
					}

				}
			}
			if (jobName != null && jobColor != null) {
				result.put(jobName, jobColor);
			}

		}

		return result;
	}

	/**
	 * Cycle all traffic light colors
	 */
	private void initTrafficLights() {
		setTrafficLightColors(500, TRAFFIC_LIGHT_COLORS.GREEN, TRAFFIC_LIGHT_COLORS.YELLOW,
				TRAFFIC_LIGHT_COLORS.RED, TRAFFIC_LIGHT_COLORS.YELLOW, TRAFFIC_LIGHT_COLORS.GREEN,
				TRAFFIC_LIGHT_COLORS.ALL);
	}

	/** {@inheritDoc} */
	@Override
	public void run() {
		try {

			while (!stopWorker) {

				log.info(String.format("polling '%s", httpget.getURI()));

				ResponseHandler<String> responseHandler = new BasicResponseHandler();
				try {
					String responseBody = httpclient.execute(httpget, responseHandler);

					Document doc = builder.parse(new InputSource(new StringReader(responseBody)));
					Object result = xpathExpression.evaluate(doc, XPathConstants.NODESET);

					Map<String, JOB_COLOR> jobStatusMap = getJobStatusMap((NodeList)result);

					if (log.isDebugEnabled()) {
						for (Map.Entry<String, JOB_COLOR> entry : jobStatusMap.entrySet()) {
							log.info(String.format("job '%s' is %s", entry.getKey(), entry.getValue()));
						}

					}

					JOB_COLOR jobColorToSet = JOB_COLOR.BLUE;
					boolean building = false;

					if (jobs.isEmpty()) {
						for (JOB_COLOR jobColor : jobStatusMap.values()) {

							if (jobColor.isBuilding()) {
								building = true;
							}

							if (jobColor.getTrafficLightColor().getSeverity() > jobColorToSet
									.getTrafficLightColor().getSeverity()) {
								jobColorToSet = jobColor;
							}

						}
					}
					else {
						for (String jobName : jobs) {
							if (jobStatusMap.containsKey(jobName)) {
								JOB_COLOR jobColor = jobStatusMap.get(jobName);

								if (jobColor.isBuilding()) {
									building = true;
								}

								if (jobColor.getTrafficLightColor().getSeverity() > jobColorToSet
										.getTrafficLightColor().getSeverity()) {
									jobColorToSet = jobColor;
								}
							}
							else {
								log.error(String.format("unknown job '%s'", jobName));
							}
						}
					}

					if (building) {
						log.info(String.format("aggregated status is '%s' (building)", jobColorToSet
								.getTrafficLightColor().toString()));
					}
					else {
						log.info(String.format("aggregated status is '%s'", jobColorToSet
								.getTrafficLightColor().toString()));
					}
					setTrafficLightColor(jobColorToSet, building);

				}
				catch (Exception e) {
					setTrafficLightColor(true, true, true);
					log.error("error getting/parsing xml api", e);
				}

				Thread.sleep(pollingInterval * 1000);
			}
		}
		catch (Exception e) {
			setTrafficLightColor(true, true, true);
			log.fatal("polling failed", e);
		}
		finally {
			httpclient.getConnectionManager().shutdown();
		}

	}

	private void setTrafficLightColor(boolean green, boolean yellow, boolean red) {

		CommandLine commandLine = CommandLine.parse(wrapperScript);
		createCommandLineArguments(commandLine, green, yellow, red);

		DefaultExecutor executor = new DefaultExecutor();
		executor.setExitValue(0);
		ExecuteWatchdog watchdog = new ExecuteWatchdog(wrapperScriptTimeout);
		executor.setWatchdog(watchdog);

		try {
			executor.execute(commandLine);

		}
		catch (Exception e) {
			throw new RuntimeException("wrapper command failed", e);
		}

	}

	private void setTrafficLightColor(JOB_COLOR jobColor, boolean building) {

		boolean green = TRAFFIC_LIGHT_COLORS.GREEN.equals(jobColor.getTrafficLightColor());

		boolean yellow;
		if (building) {
			yellow = true;
		}
		else {
			yellow = TRAFFIC_LIGHT_COLORS.YELLOW.equals(jobColor.getTrafficLightColor());
		}

		boolean red = TRAFFIC_LIGHT_COLORS.RED.equals(jobColor.getTrafficLightColor());

		setTrafficLightColor(green, yellow, red);
	}

	private void setTrafficLightColor(TRAFFIC_LIGHT_COLORS trafficLightColor) {
		switch (trafficLightColor) {
		case GREEN:
			setTrafficLightColor(true, false, false);
			break;
		case YELLOW:
			setTrafficLightColor(false, true, false);
			break;
		case RED:
			setTrafficLightColor(false, false, true);
			break;
		default:
			setTrafficLightColor(true, true, true);
			break;
		}
	}

	private void setTrafficLightColors(int delay, TRAFFIC_LIGHT_COLORS... trafficLightColors) {
		try {

			for (TRAFFIC_LIGHT_COLORS trafficLightColor : trafficLightColors) {
				setTrafficLightColor(trafficLightColor);
				Thread.sleep(delay);
			}
		}
		catch (Exception e) {
			log.error("traffic light init failed", e);
			System.exit(1);
		}
	}

}
